package com.leyou.search.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.leyou.item.pojo.*;
import com.leyou.search.client.BrandClient;
import com.leyou.search.client.CategoryClient;
import com.leyou.search.client.GoodsClient;
import com.leyou.search.client.SpecClient;
import com.leyou.search.pojo.Goods;
import com.leyou.search.pojo.SearchRequest;
import com.leyou.search.pojo.SearchResult;
import com.leyou.search.repository.GoodsRepository;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.LongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.util.*;

/**
 * *
 * Created by IntelliJ IDEA.
 * Author: caoLei
 * Date: 2018/10/29
 * Time: 9:46
 * *
 */
@Service
public class SearchService {

    @Autowired
    private CategoryClient categoryClient;

    @Autowired
    private BrandClient brandClient;

    @Autowired
    private GoodsClient goodsClient;

    @Autowired
    private SpecClient specClient;

    @Autowired
    private GoodsRepository goodsRepository;

    private static final Logger logger = LoggerFactory.getLogger(SearchService.class);

    //jackson提供的将对象序列化为字符串的一个工具
    private static final ObjectMapper MAPPER = new ObjectMapper();


    public Goods buildGoods(Spu spu) throws IOException {

        //根据cid1，cid2，cid3查询商品分类
        List<String> names = this.categoryClient.queryNameByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));

        //根据brandid查询品牌
        Brand brand = this.brandClient.queryBrandById(spu.getBrandId());

        //根据spuid查询该spu下的所有sku
        List<Sku> skus = this.goodsClient.querySkusBySpuId(spu.getId());

        //设置sku，搜集所有的list<sku>,序列化为字符串，
        //构建skuMap集合，用来储存需要的4个数据（id,title,image,price）
        List<Map<String, Object>> skuMapList = new ArrayList<>();

        //查询sku中所有的价格，并保存在一个集合中
        List<Long> prices = new ArrayList<>();
        skus.forEach(sku -> {
            prices.add(sku.getPrice());
            Map<String, Object> map = new HashMap<>();
            map.put("id", sku.getId());
            map.put("title", sku.getTitle());
            map.put("image", StringUtils.isBlank(sku.getImages()) ? "" : StringUtils.split(sku.getImages(), ",")[0]);
            map.put("price", sku.getPrice());
            skuMapList.add(map);

        });

        //根据cid3设置规格参数
        List<SpecParam> params = this.specClient.querySpecParams(null, spu.getCid3(), true, null);
        //我们需要spu_detail中的两个字段（generic_spec,special_spec）
        //根据spuid查询spudetail
        SpuDetail spuDetail = this.goodsClient.querySpuDetailById(spu.getId());
        //获取spudatail中的通用规格参数 （由于参数是json，所以我们需要反序列化）
        Map<String, Object> genericMap = MAPPER.readValue(spuDetail.getGenericSpec(), new TypeReference<Map<String, Object>>() {
        });
        //获取spudetail中的特殊规格参数
        Map<String, List<Object>> specialMap = MAPPER.readValue(spuDetail.getSpecialSpec(), new TypeReference<Map<String, List<Object>>>() {
        });
        //搜索规格参数及参数值集合
        //第二个参数，有可能时string，list，所以用object接收
        Map<String, Object> searchMap = new HashMap<>();
        params.forEach(param -> {
            //如果是通用参数
            if (param.getGeneric()) {
                String value = genericMap.get(param.getId().toString()).toString();
                if (param.getNumeric()) {
                    value = chooseSegment(value, param);
                }
                searchMap.put(param.getName(), value);
                //特殊规格参数
            } else {
                List<Object> value = specialMap.get(param.getId().toString());
                searchMap.put(param.getName(), value);
            }
        });

        Goods goods = new Goods();
        goods.setId(spu.getId());
        goods.setCid1(spu.getCid1());
        goods.setCid2(spu.getCid2());
        goods.setCid3(spu.getCid3());
        goods.setCreateTime(spu.getCreateTime());
        goods.setBrandId(spu.getBrandId());
        //商品标题，分类，品牌，
        goods.setAll(spu.getTitle() + " " + StringUtils.join(names, " ") + brand.getName());
        //sku中所有价格的集合(list<long>)，方便以价格进行搜索
        goods.setPrice(prices);
        //设置sku，搜集所有的list<sku>,序列化为字符串
        goods.setSkus(MAPPER.writeValueAsString(skuMapList));
        //设置规格参数，map<参数名，参数值>
        goods.setSpecs(searchMap);

        return goods;
    }

    private String chooseSegment(String value, SpecParam p) {
        double val = NumberUtils.toDouble(value);
        String result = "其它";
        // 保存数值段
        for (String segment : p.getSegments().split(",")) {
            String[] segs = segment.split("-");
            // 获取数值范围
            double begin = NumberUtils.toDouble(segs[0]);
            double end = Double.MAX_VALUE;
            if (segs.length == 2) {
                end = NumberUtils.toDouble(segs[1]);
            }
            // 判断是否在范围内
            if (val >= begin && val < end) {
                if (segs.length == 1) {
                    result = segs[0] + p.getUnit() + "以上";
                } else if (begin == 0) {
                    result = segs[1] + p.getUnit() + "以下";
                } else {
                    result = segment + p.getUnit();
                }
                break;
            }
        }
        return result;
    }

    public SearchResult search(SearchRequest request) {

        String key = request.getKey();
        // // 判断是否有搜索条件，如果没有，直接返回null。不允许搜索全部商品
        if (StringUtils.isBlank(key)) {
            return null;
        }

        //构建查询条件 new NativeSearchQueryBuilder
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        // 1、对key进行全文检索查询
        BoolQueryBuilder basicQuery=buildBasicQueryWithFilter(request);

        queryBuilder.withQuery(basicQuery);

        // 2、通过sourceFilter设置返回的结果字段,我们只需要id、skus、subTitle
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "skus", "title"}, null));

        //分页，排序
        searchWithPageAndSort(request, queryBuilder);

        //聚合
        String categoryAggName = "category";//商品分类聚合名称
        String brandAggName = "brand";//品牌分类聚合名称

        //对商品分类进行聚合
        queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));

        //对品牌分类进行聚合
        queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));

        // 4、查询，获取结果
        AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>) this.goodsRepository.search(queryBuilder.build());

        // 封装结果并返回
        //总条数
        Long total = goodsPage.getTotalElements();
        //总页数
        Integer totalPage = (total.intValue() + request.getSize() - 1) / request.getSize();

        //商品分类的聚合结果
        List<Map<String, Object>> categories = getCategoryAggResult(goodsPage.getAggregation(categoryAggName));

        //品牌分类的聚合结果
        List<Brand> brands = getBrandAggResult(goodsPage.getAggregation(brandAggName));

        //根据商品分类判断是否需要聚合
        List<Map<String, Object>> specs = new ArrayList<>();
        if (categories.size() == 1) {
            //根据分类id确定哪些规格参数需要聚合，根据基本查询条件聚合出可选值，如小米手机和华为手机聚合出的值是不同的
            specs = getSpecAggResult((Long) categories.get(0).get("id"), basicQuery);
        }

        return new SearchResult(goodsPage.getTotalElements(), goodsPage.getTotalPages(), goodsPage.getContent(),categories,brands,specs);
    }

    /**
     * 构建基本查询条件
     * @param request
     * @return
     */
    private BoolQueryBuilder buildBasicQueryWithFilter(SearchRequest request) {
        //创建bool查询
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //基本查询条件
        MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("all", request.getKey());
        //将基本查询条件放入组合查询
        boolQueryBuilder.must(matchQueryBuilder);
        //将过滤查询放入组合查询
        Map<String, String> filter = request.getFilter();
        if (CollectionUtils.isEmpty(filter)) {
            return boolQueryBuilder;
        }
        //遍历map
        for (Map.Entry<String, String> entry : filter.entrySet()) {
            String key = entry.getKey();
            if (StringUtils.equals("品牌", key)) {
                //如果是品牌，过滤字段是brandId
                key = "brandId";
            } else if (StringUtils.equals("分类", key)) {
                //如果是分类，过滤字段是cid3
                key = "cid3";
            } else {
                //如果是普通的规格参数，过滤字段是规格参数名 specs.key.keyword
                key = "specs." + key + ".keyword";
            }
            boolQueryBuilder.filter(QueryBuilders.termsQuery(key, entry.getValue()));
        }
        return boolQueryBuilder;
    }

    /**
     * 聚合规格参数
     * @param cid
     * @param basicQuery
     * @return
     */
    private List<Map<String,Object>> getSpecAggResult(Long cid, BoolQueryBuilder basicQuery) {
        //查询要聚合的规格参数
        List<SpecParam> params = this.specClient.querySpecParams(null, cid, true, null);
        //初始化自定义查询构建器
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        //基于基本的查询条件聚合
        queryBuilder.withQuery(basicQuery);
        //添加结果集过滤，不需要普通的查询结果集
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{}, null));

        //遍历要聚合的规格参数，进行聚合
        params.forEach(param->{
            String key = param.getName();
            //添加规格参数聚合，以规格参数名为聚合名称，
            queryBuilder.addAggregation(AggregationBuilders.terms(key).field("specs." + key + ".keyword"));
        });
        //执行查询，获取聚合结果集
        AggregatedPage<Goods> paramAggPage = (AggregatedPage<Goods>) this.goodsRepository.search(queryBuilder.build());
        //key--聚合名称：规格参数名  value:聚合结果
        Map<String, Aggregation> paramAggMap = paramAggPage.getAggregations().asMap();

        List<Map<String, Object>> specs = new ArrayList<>();
        //解析聚合结果
        for (Map.Entry<String, Aggregation> aggregationEntry : paramAggMap.entrySet()) {
            Map<String, Object> map = new HashMap<>();
            map.put("k", aggregationEntry.getKey());
            List<String> options = new ArrayList<>();
            StringTerms paramAgg = (StringTerms)aggregationEntry.getValue();
            paramAgg.getBuckets().forEach(bucket -> {
                options.add(bucket.getKeyAsString());
            });
            map.put("options", options);
            specs.add(map);
        }
        return specs;
    }

    /**
     * 解析品牌分类聚合结果
     *
     * @param aggregation
     * @return
     */
    private List<Brand> getBrandAggResult(Aggregation aggregation) {
        try {
            LongTerms brandAgg = (LongTerms) aggregation;
            List<Long> bids = new ArrayList<>();
            for (LongTerms.Bucket bucket : brandAgg.getBuckets()) {
                bids.add(bucket.getKeyAsNumber().longValue());
            }
            // 根据id查询品牌
            return this.brandClient.queryBrandByIds(bids);
        } catch (Exception e){
            logger.error("品牌聚合出现异常：", e);
            return null;
        }
    }

    /**
     * 解析商品分类聚合结果
     *
     * @param aggregation
     * @return
     */
    private List<Map<String, Object>> getCategoryAggResult(Aggregation aggregation) {

        try {
            List<Map<String, Object>> categories = new ArrayList<>();
            LongTerms categoryAgg = (LongTerms) aggregation;
            List<Long> cids = new ArrayList<>();
            for (LongTerms.Bucket bucket : categoryAgg.getBuckets()) {
                cids.add(bucket.getKeyAsNumber().longValue());
            }
            //根据id查询分类名称
            List<String> names = this.categoryClient.queryNameByIds(cids);

            //遍历names
            for (int i = 0; i < names.size(); i++) {
                Map<String, Object> map = new HashMap<>();
                map.put("id", cids.get(i));
                map.put("name", names.get(i));
                categories.add(map);
            }
            return categories;
        } catch (Exception e) {

            logger.info("分类聚合出现异常：",e);
            return null;
        }
    }

    /**
     * 封装分页，排序方法
     *
     * @param request
     * @param queryBuilder
     */
    private void searchWithPageAndSort(SearchRequest request, NativeSearchQueryBuilder queryBuilder) {
        // 3、分页
        Integer page = request.getPage() - 1;
        Integer size = request.getSize();
        queryBuilder.withPageable(PageRequest.of(page, size));

        //排序
        String sortBy = request.getSortBy();
        Boolean desc = request.getDescending();  //desc为true时，降序
        if (StringUtils.isNotBlank(sortBy)) {
            queryBuilder.withSort(SortBuilders.fieldSort(sortBy).order(desc ? SortOrder.DESC : SortOrder.ASC));
        }

    }

    public void save(Long spuId) throws IOException {//这里的异常应该抛出去，不应该try-catch（消息监听，异常被处理后，则消息也被处理，此时数据没有被同步），抛到controller，spring监听到有异常，spring的aop会处理，自动回滚
        Spu spu = this.goodsClient.querySpuBySpuId(spuId);
        Goods goods = this.buildGoods(spu);
        this.goodsRepository.save(goods);
    }
}
